src/Security/CustomerAuthenticator.php line 58

Open in your IDE?
  1. <?php
  2. namespace App\Security;
  3. use App\Controller\Services\CaptchaController;
  4. use App\Entity\Account;
  5. use App\Entity\EgeeClient;
  6. use Psr\Log\LoggerInterface;
  7. use Doctrine\ORM\EntityManagerInterface;
  8. use App\Domain\Encryption\HashingService;
  9. use Symfony\Component\HttpFoundation\Request;
  10. use Symfony\Component\Security\Core\Security;
  11. use Symfony\Component\Security\Csrf\CsrfToken;
  12. use App\Exception\RecaptchaVerificationException;
  13. use Symfony\Component\HttpFoundation\RedirectResponse;
  14. use Symfony\Component\Security\Core\User\UserInterface;
  15. use Symfony\Component\Security\Http\Util\TargetPathTrait;
  16. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  17. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  18. use Symfony\Component\Security\Core\User\UserProviderInterface;
  19. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  20. use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
  21. use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
  22. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  23. class CustomerAuthenticator extends AbstractFormLoginAuthenticator
  24. {
  25.     use TargetPathTrait;
  26.     public const LOGIN_ROUTE 'login';
  27.     private $entityManager;
  28.     private $urlGenerator;
  29.     private $csrfTokenManager;
  30.     private $hashingService;
  31.     private $logger;
  32.     public function __construct(
  33.         EntityManagerInterface $entityManager,
  34.         UrlGeneratorInterface $urlGenerator,
  35.         CsrfTokenManagerInterface $csrfTokenManager,
  36.         HashingService $hashingService,
  37.         LoggerInterface $logger
  38.     ) {
  39.         $this->entityManager $entityManager;
  40.         $this->urlGenerator $urlGenerator;
  41.         $this->csrfTokenManager $csrfTokenManager;
  42.         $this->hashingService $hashingService;
  43.         $this->logger $logger;
  44.     }
  45.     public function supports(Request $request)
  46.     {
  47.         return self::LOGIN_ROUTE === $request->attributes->get('_route')
  48.             && $request->isMethod('POST');
  49.     }
  50.     public function getCredentials(Request $request)
  51.     {
  52.         $ip $_SERVER['REMOTE_ADDR'];
  53.         $dateDuJour date("Ymd");
  54.         $varPath getcwd().'/../var';
  55.         $chemin $varPath.'/captcha';
  56.         if (!is_dir($chemin)) {
  57.             // Crée le dossier avec les permissions 0755, et true pour créer les dossiers parents si nécessaire
  58.             if (!mkdir($chemin0755true)) {
  59.                 die('Échec lors de la création du dossier...');
  60.             }
  61.         }
  62.         $fail_log $chemin '/'.$dateDuJour.'.log';
  63.         $max_attempts 5;
  64.         $time_window 15 60// 15 minutes
  65.         // ❗ Vérifie si l’IP est déjà bloquée
  66.         if ($this->verifierSiIpBloquee($ip$fail_log$max_attempts$time_window)) {
  67.             $this->logger->info('reCAPTCHA rejeté', ['IP bloquée']);
  68.             throw new RecaptchaVerificationException();
  69.             //die("Trop de tentatives échouées. Réessayez plus tard.");
  70.         }
  71.         /*
  72.          * Clé Google Recaptcha v3
  73.          * DEV :
  74.          *      - clé secrète 6LcnjyoaAAAAAPhi6K_AxoW47WPWJHNQCDEgTtMS
  75.          *      - clé site 6LcnjyoaAAAAANHwcvnepkwrR3Xby2e7FTPTTG_r
  76.          *
  77.          * PROD :
  78.          *      - clé secrète 6LcjorMZAAAAAPZ2jHNngd9MpmsUcO9pv6oNB3yx
  79.          *      - clé site 6LcjorMZAAAAAEKYV5kfyGo_K-oBN_dZGXLAs4N3
  80.          */
  81.         //if (null !== $request->request->get('recaptcha_response')) {
  82.         if (isset($_POST['formulaire_jeton']) && $_POST['formulaire_jeton'] == $_SESSION['captcha_token']) {
  83.             if (time() - $_SESSION['formulaire_time'] < 3) {
  84.                 $this->logger->info('reCAPTCHA rejeté', ['Soumission trop rapide : bot probable']);
  85.                 throw new RecaptchaVerificationException();
  86.             }
  87.             $trap_field $_SESSION['formulaire_piege'] ?? '';
  88.             if (!empty($_POST[$trap_field])) {
  89.                 $this->logger->info('reCAPTCHA rejeté', ['Bot détecté (champ dynamique)']);
  90.                 throw new RecaptchaVerificationException();
  91.             }
  92.             //$recaptcha_url = 'https://www.google.com/recaptcha/api/siteverify';
  93.             /*$host = $request->getHost();
  94.             switch ($host) {
  95.                 case 'ec-eau-2024.iti-communication.net':
  96.                     $recaptcha_secret = '6LcnjyoaAAAAAPhi6K_AxoW47WPWJHNQCDEgTtMS';
  97.                     break;
  98.                 default:
  99.                     $recaptcha_secret = '6LcjorMZAAAAAPZ2jHNngd9MpmsUcO9pv6oNB3yx';
  100.             }
  101.             $recaptcha_response = $request->request->get('recaptcha_response');
  102.             $params = [
  103.                 'secret' => $recaptcha_secret,
  104.                 'response' => $recaptcha_response,
  105.             ];*/
  106.             // Requete de vérification du score
  107.             /*$ch = curl_init($recaptcha_url);
  108.             curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  109.             curl_setopt($ch, CURLOPT_TIMEOUT, 2); // timeout max 2s
  110.             curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
  111.             $start = microtime(true);
  112.             $response = curl_exec($ch);
  113.             $duration = microtime(true) - $start;
  114.             if ($response === false) {
  115.                 $error = curl_error($ch);
  116.                 curl_close($ch);
  117.                 $this->logger->error('reCAPTCHA API error: ' . $error);
  118.                 throw new RecaptchaVerificationException();
  119.             }
  120.             curl_close($ch);
  121.             $recaptcha = json_decode($response);
  122.             if ($duration > 1.5) {
  123.                 $this->logger->warning('reCAPTCHA API slow response', ['duration' => $duration]);
  124.             }*/
  125.             //if (isset($recaptcha->score) && $recaptcha->score >= 0.5) {
  126.             //    $this->logger->info('reCAPTCHA accepté', ['score' => $recaptcha->score ?? 'null']);
  127.                 $this->logger->info('reCAPTCHA accepté', ['OK']);
  128.                 $credentials = [
  129.                     'reference' => $request->request->get('reference'),
  130.                     'password' => $request->request->get('password'),
  131.                     'csrf_token' => $request->request->get('_csrf_token'),
  132.                 ];
  133.                 $request->getSession()->set(
  134.                     Security::LAST_USERNAME,
  135.                     $credentials['reference']
  136.                 );
  137.                 return $credentials;
  138.             /*} else {
  139.                 $this->logger->info('reCAPTCHA rejeté', ['score' => $recaptcha->score ?? 'null']);
  140.                 throw new RecaptchaVerificationException();
  141.             }*/
  142.         } else {
  143.             $this->loguerEchecs($ip$fail_log);
  144.             $this->logger->info('reCAPTCHA rejeté', ['Captcha incorrect']);
  145.             //throw new RecaptchaVerificationException();
  146.             throw new InvalidCsrfTokenException('reCAPTCHA manquant.');
  147.         }
  148.     }
  149.     public function getUser($credentialsUserProviderInterface $userProvider)
  150.     {
  151.         $token = new CsrfToken('authenticate'$credentials['csrf_token']);
  152.         if (!$this->csrfTokenManager->isTokenValid($token)) {
  153.             throw new InvalidCsrfTokenException();
  154.         }
  155.         $account $this->entityManager->getRepository(Account::class)->getAccountByReference(
  156.             $credentials['reference']
  157.         );
  158.         $client $this->entityManager->getRepository(EgeeClient::class)->findOneBy(
  159.             ['cliReference' => $credentials['reference']]
  160.         );
  161.         if (!$account) { // Si usager non présent dans la table account on cherche si présent dans l'ancienne plareforme
  162.             $varPath getcwd().'/../var';
  163.             $accountsLegacy = [];
  164.             if (file_exists($varPath.'/account')) {
  165.                 $fileAccount fopen($varPath.'/account''r');
  166.                 while (!feof($fileAccount)) {
  167.                     $line fgets($fileAccount);
  168.                     $line explode(','$line);
  169.                     if (isset($line[0]) && isset($line[1]) && isset($line[2])) {
  170.                         $accountsLegacy[$line[0]] = [
  171.                             'password' => $line[1],
  172.                             'email' => $line[2],
  173.                         ];
  174.                     }
  175.                 }
  176.                 fclose($fileAccount);
  177.             }
  178.             if (isset($accountsLegacy[$credentials['reference']]) && isset($client)) { // Si present dans le fichier account de rétrocompatibilité
  179.                 $accountEmail preg_replace("/\r|\n/"''$accountsLegacy[$credentials['reference']]['email']);
  180.                 if ('' == $accountEmail && $client->getCliMel()) {
  181.                     $accountEmail $client->getCliMel();
  182.                 }
  183.                 if (crypt(
  184.                     $credentials['password'],
  185.                     $accountsLegacy[$credentials['reference']]['password']
  186.                 ) === $accountsLegacy[$credentials['reference']]['password']) {
  187.                     // Si authentification rétrocompatible réussie on créer une entrée dans la table account
  188.                     $this->createAccountRetroCompatibility(
  189.                         $credentials['reference'],
  190.                         $accountEmail,
  191.                         $credentials['password']
  192.                     );
  193.                     // On supprime l'entree dans le fichier
  194.                     unset($accountsLegacy[$credentials['reference']]);
  195.                     $fileAccount fopen($varPath.'/account''w');
  196.                     foreach ($accountsLegacy as $key => $line) {
  197.                         fwrite($fileAccount$key.','.$line['password'].','.$line['email']);
  198.                     }
  199.                     fclose($fileAccount);
  200.                     $account $this->entityManager->getRepository(Account::class)->getAccountByReference(
  201.                         $credentials['reference']
  202.                     );
  203.                     $this->logger->info(
  204.                         'Connexion de l\'utilisateur - Création de l\'usager (Retrocompatibilité du mot de passe)',
  205.                         ['customer' => $credentials['reference']]
  206.                     );
  207.                 } else {
  208.                     // Si l'authentification via la rétrocompatibilité à échouée on créer une entrée dans la table account
  209.                     $this->createAccountRetroCompatibility(
  210.                         $credentials['reference'],
  211.                         $accountEmail,
  212.                         'qU39npeP4mVaB655atRS4J6S'
  213.                     );
  214.                     $this->logger->error(
  215.                         'Connexion de l\'utilisateur - Echec authentification rétrocompatible, création user verrouillé',
  216.                         ['customer' => $credentials['reference']]
  217.                     );
  218.                     throw new CustomUserMessageAuthenticationException('La référence client est introuvable, en cas de problème veuillez utiliser le lien "mot de passe oublié"');
  219.                 }
  220.             } elseif ($client) { // Si usager non présent dans la table account, non présent dans le fichier account mais present dans la table client
  221.                 if ($client->getCliMel()) {
  222.                     $accountEmail $client->getCliMel();
  223.                 } else {
  224.                     $accountEmail '';
  225.                 }
  226.                 if ($client->getCliTelephone()) {
  227.                     $accountTel $client->getCliTelephone();
  228.                 } else {
  229.                     $accountTel '';
  230.                 }
  231.                 if (crypt(
  232.                     $credentials['password'],
  233.                     $client->getCliMdp()
  234.                 ) === $client->getCliMdp()) {
  235.                     $this->createAccountRetroCompatibility(
  236.                         $credentials['reference'],
  237.                         $accountEmail,
  238.                         $credentials['password'],
  239.                         $accountTel
  240.                     );
  241.                     $account $this->entityManager->getRepository(Account::class)->getAccountByReference(
  242.                         $credentials['reference']
  243.                     );
  244.                     $this->logger->info(
  245.                         'Connexion de l\'utilisateur - Création de l\'usager (Account non présent)',
  246.                         ['customer' => $credentials['reference']]
  247.                     );
  248.                 } else {
  249.                     // Si l'authentification via la récupération des infos de la table client à échouée on créer une entrée dans la table account
  250.                     $this->createAccountRetroCompatibility(
  251.                         $credentials['reference'],
  252.                         $accountEmail,
  253.                         'qU39npeP4mVaB655atRS4J6S',
  254.                         $accountTel
  255.                     );
  256.                     $this->logger->error(
  257.                         'Connexion de l\'utilisateur - Echec authentification via récupération des infos table client, création user verrouillé',
  258.                         ['customer' => $credentials['reference']]
  259.                     );
  260.                     throw new CustomUserMessageAuthenticationException('La référence client est introuvable, en cas de problème veuillez utiliser le lien "mot de passe oublié"');
  261.                 }
  262.             } else { // usager inconnu (non présent dans le fichier account ni dans la table client) ou authentification incorrecte
  263.                 $this->logger->error(
  264.                     'Connexion de l\'utilisateur - Echec référence introuvable',
  265.                     ['customer' => $credentials['reference']]
  266.                 );
  267.                 throw new CustomUserMessageAuthenticationException('La référence client est introuvable');
  268.             }
  269.         }
  270.         return $account;
  271.     }
  272.     private function createAccountRetroCompatibility($reference$email$password$telephone '')
  273.     {
  274.         $account = new Account();
  275.         $account->setReference($reference);
  276.         $account->setEmail($email);
  277.         if ($telephone) {
  278.             $account->setPhoneNumber($telephone);
  279.         }
  280.         $account->setPassword($this->hashingService->hashPassword($password));
  281.         $account->setIsSetupNeeded(1);
  282.         $account->setUpdatedAt(new \DateTime());
  283.         $this->entityManager->persist($account);
  284.         $this->entityManager->flush();
  285.     }
  286.     public function checkCredentials($credentialsUserInterface $account)
  287.     {
  288.         return password_verify($credentials['password'], $account->getPassword());
  289.     }
  290.     public function onAuthenticationSuccess(Request $requestTokenInterface $token$providerKey)
  291.     {
  292.         $account $this->entityManager->getRepository(Account::class)->find($token->getUser()->getAutoId());
  293.         $varPath getcwd().'/../var';
  294.         $reference preg_replace('/\s+/'''$account->getReference());
  295.         $dossierParent substr($reference03);
  296.         $filename $varPath.'/accounts/'.$dossierParent.'/'.$reference;
  297.         if (!file_exists($varPath.'/accounts')) {
  298.             mkdir($varPath.'/accounts'0777true);
  299.         }
  300.         if (!file_exists($varPath.'/accounts/'.$dossierParent)) {
  301.             mkdir($varPath.'/accounts/'.$dossierParent0777true);
  302.         }
  303.         if (!file_exists($filename)) {
  304.             $timestamp time();
  305.             $result file_put_contents($filename$timestamp);
  306.             if (false !== $result) {
  307.                 $this->logger->info('Creation fichier timestamp', ['customer' => $account->getReference()]);
  308.             } else {
  309.                 $this->logger->info('Impossible d\'écrire dans le fichier timestamp', ['customer' => $account->getReference()]);
  310.             }
  311.         }
  312.         // --------------------------------------------------
  313.         // On ajoute une vérification pour forcer le
  314.         // changement de mot de passe au bout de 150 jours
  315.         /*$derniereMaj = file_get_contents($filename);
  316.         $delaiMaj = $derniereMaj + (86400 * 150);
  317.         if($delaiMaj < time()) {
  318.             $account->setIsSetupNeeded(1);
  319.             $this->entityManager->flush();
  320.         }*/
  321.         // --------------------------------------------------
  322.         if ($account->getIsSetupNeeded()) {
  323.             $this->logger->info(
  324.                 'Connexion de l\'utilisateur - Réussite première configuration',
  325.                 ['customer' => $account->getReference()]
  326.             );
  327.             return new RedirectResponse($this->urlGenerator->generate('account-setup'));
  328.         }
  329.         $this->logger->info('Connexion de l\'utilisateur - Réussite', ['customer' => $account->getReference()]);
  330.         if ($targetPath $this->getTargetPath($request->getSession(), $providerKey)) {
  331.             return new RedirectResponse($targetPath);
  332.         }
  333.         return new RedirectResponse($this->urlGenerator->generate('dashboard'));
  334.     }
  335.     protected function getLoginUrl()
  336.     {
  337.         return $this->urlGenerator->generate(self::LOGIN_ROUTE);
  338.     }
  339.     private function hashLegacyPassword($password$cost)
  340.     {
  341.         $salt substr(base64_encode(openssl_random_pseudo_bytes(17)), 022);
  342.         $salt str_replace('+''.'$salt);
  343.         $param '$'.implode('$', ['2y'str_pad($cost2'0'STR_PAD_LEFT), $salt]);
  344.         return crypt($password$param);
  345.     }
  346.     private function verifierSiIpBloquee($ip$fail_log$max_attempts$time_window) {
  347.         if (!file_exists($fail_log)) return false;
  348.         $lines file($fail_logFILE_IGNORE_NEW_LINES FILE_SKIP_EMPTY_LINES);
  349.         $now time();
  350.         $recent_fails 0;
  351.         foreach ($lines as $line) {
  352.             list($logged_ip$timestamp) = explode('|'$line);
  353.             if ($logged_ip === $ip && ($now - (int)$timestamp) < $time_window) {
  354.                 $recent_fails++;
  355.             }
  356.         }
  357.         return $recent_fails >= $max_attempts;
  358.     }
  359.     private function loguerEchecs($ip$fail_log) {
  360.         file_put_contents($fail_log"$ip|" time() . "\n"FILE_APPEND);
  361.     }
  362. }